Home Making an Install Script - Parts 2 & 3: Wrapping up
Post
Cancel

Making an Install Script - Parts 2 & 3: Wrapping up

Parts 2 & 3 - Wrapping up

Here are some things I had to learn when building distribution for azd.

Note This blog series was started in 2022, and the Azure Developer CLI plus its installer story have changed considerably since then. Let’s get this finished so I can go on to writing other things.

Part 2: Download and install

The azd installers come in two flavors: Bash and cross-platform PowerShell. This gives us good coverage on our supported OS platforms.

PlatformBashPowerShell
Windows🚫 Not by Default✅ Default
Linux✅ Default🚫 Not by Default
MacOS⚠️ Bash 3🚫 Not by Default

Most non-Windows platforms have bash or a straightforward way to install bash.

⚠️ MacOS uses bash 3. So our script needs to run without some of the fancy features available in bash 4 (like associative arrays).

PowerShell, MSI, Cross Platform installs

On Windows we switched to using an MSI and in the install script uses msiexec to install. If the PowerShell script is running on a non-Windows platform it uses the bash install script with equivalent parameters.

2.1. Bash fun: Detecting platform and architecture

The current bash installer pulls apart the download target into three pieces:

  • platform (linux or darwin)
  • archive format (tar.gz or zip)
  • CPU architecture (amd64 or arm64)

It gets all of that from the machine it is currently running on.

Detecting the platform

The installer asks uname -s for the operating system name and then normalizes that value into the names used in the published artifact filenames. uname is available on all platforms tested.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
get_platform() {
    local platform_raw
    platform_raw="$(uname -s)"

    if [ "$platform_raw" = "Linux" ]; then
        echo 'linux'
        return 0
    elif [ "$platform_raw" = "Darwin" ]; then
        echo 'darwin'
        return 0
    else
        say_error "Platform not supported: $platform_raw"
        return 1
    fi
}

There are only two successful paths here:

  • Linux becomes linux
  • Darwin becomes darwin

Anything else is treated as unsupported and the script stops. No support today for BSD or other platforms.

Choosing the archive extension

Once the platform is known, the installer uses it to decide which kind of archive it should download.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
get_extension_for_platform() {
    platform=$1

    if [ "$platform" = "linux" ]; then
        echo 'tar.gz'
        return
    elif [ "$platform" = "darwin" ]; then
        echo 'zip'
        return
    else
        say_error "Platform not supported: $platform"
        exit 1
    fi
}

That means Linux downloads a .tar.gz archive, while macOS downloads a .zip.

Detecting the architecture

The CPU architecture comes from uname -m. The script then maps a few common machine identifiers to the names used by the release pipeline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
get_architecture() {
    local platform=$1
    local architecture_raw
    architecture_raw="$(uname -m)"

    if [ "$architecture_raw" = "x86_64" ]; then
        echo 'amd64'
        return
    elif [ "$architecture_raw" = "arm64" ]; then
        echo "$architecture_raw"
        return
    elif [ "$architecture_raw" = "aarch64" ]; then
        echo 'arm64'
        return
    else
        say_error "Architecture not supported: $architecture_raw on platform: $platform"
        exit 1
    fi
}

So today the mapping is:

  • x86_64amd64
  • arm64arm64
  • aarch64arm64

One notable change from the original 2022 version: Apple Silicon is no longer forced onto the amd64 build. If uname -m reports arm64, the installer now downloads the native arm64 binary.

Putting it together

The installer wires those pieces up right away:

1
2
3
4
5
6
platform="$(get_platform)"
extension="$(get_extension_for_platform "$platform")"

if [ "${architecture:-}" = "" ]; then
    architecture="$(get_architecture "$platform")"
fi

By the time URL construction happens, the script already knows the normalized platform, the right archive extension, and the architecture string to plug into the download path.

2.2. Download the binary

The URL format, which hasn’t changed much since 2022, is:

1
https://<base-url>/<version>/azd-<platform>-<architecture>.<extension>
  • <base-url> is the base part of the URL used to download releases (e.g. https://azuresdkartifacts.z5.web.core.windows.net/azd/standalone/release)
  • <version> represents either a specific version or a “channel” of release. Today that means daily, stable, or a specific version like 1.23.7

⚠️ Warning: The <base-url> has changed several times as we’ve migrated hosting providers and storage accounts. Don’t rely on the host name you see here as it could easily change in the future.

You’ll notice that the URL assembly process is specific to the publishing system that we’ve built. You’ll want to assemble URLs specific to your system. Ours gives us flexibility to assemble install stanzas for a variety of practical scenarios that would exercise our install script in PRs (similar to how we would exercise the azd tool itself in pull requests).

In bash

In bash we use curl to download the file (curl is implied to be on the system in the install stanza itself curl -fsSL https://aka.ms/install-azd.sh | bash).

In PowerShell

We do similar platform detection, it’s easier on Windows because the install script supports only x86_64 today so there’s one MSI to download.

2.3. Copy the binary to a standard location, install the MSI

The /opt folder is generally used for optional software packages on Linux and MacOS. It’s not specifically in $PATH so we’ll need to symlink the binary in a location that’s already in $PATH.

/usr/local/bin is a good spot to link. It’s a common location for binaries installed on the local system and is already included in $PATH on tested systems. There are corner cases on MacOS where it’s in $PATH but the directory itself doesn’t exist.

In that case, we detect it’s not there and report an error with instructions to the user on how to fix it.

1
2
3
4
5
6
if [ "$symlink_folder" != "" ] && [ ! -d "$symlink_folder" ]; then
    say_error "Symlink folder does not exist: $symlink_folder. The symlink folder should exist and be in \$PATH"
    say_error "Create the folder (and ensure that it is in your \$PATH), specify a different folder using -s or --symlink-folder, or specify an empty value using -s \"\" or --symlink-folder \"\""
    save_error_report_if_enabled "InstallFailed" "SymlinkFolderDoesNotExist"
    exit 1
fi

Why not copy the binary directly to /usr/local/bin?

The zip package includes other files that are required to be placed alongside the installed binary. Instead of writing those files to a location where the intent of those files can be ambiguous and not directly useful to the user’s day to day tasks, we place the full installation in /opt/microsoft/azd and link the relevant binary from /usr/local/bin.

I want to install in ~/.bin

The install bash script supports both --install-folder and --symlink-folder parameters. Make sure you specify those properly each time you upgrade otherwise the installer will use the default configuration.

Windows: Install the MSI

Use msiexec and Wait-Process so that the script doesn’t finish before the installation completes. You can also evaluate the exit code of msiexec to determine if the installation succeeded or failed.

Part 3: Other distribution mechanisms

WinGet & Chocolatey

One of the early benefits of using MSI is that we can easily onboard to WinGet and Chocolatey, two popular package managers on Windows.

For WinGet, we use wingetcreate.exe to create a manfiest and open a PR to the winget-pkgs repository. Once the PR is merged, the package is available in the WinGet repository.

For Chocolatey, we use choco new to create a package and then choco push to publish it to the Chocolatey repository.

Homebrew

We created a Homebrew tap to distribute azd on MacOS. The tap is a GitHub repository that contains the formula for installing azd. The formula is a Ruby file that defines how to download and install azd on MacOS.

Later edits include Linux support which means that users can now brew install azd on Linux as well.

Part 4: Testing

I found setting up Dockerfile-based tests to be helpful for testing azd installation in various types of popular Linux distributions. Azure DevOps and GitHub Actions both offer MacOS and Windows machines for more complete coverage.

Since install tests can be automated, I’m able to run these tests before release to catch any issues with the install process before customers do.

Install testing has detected more platform-specific issues with the azd binary itself than with the installer.

ShellCheck

Use ShellCheck on your bash scripts to catch common issues. I’m already bad at writing bash scripts but ShellCheck makes me look like a very smart cookie.

Special Thanks

Heath Stewart is responsible for much of the MSI work to date and his knowledge has been indispensable to the success of azd’s growth and distribution. He also helped with other parts of the install process. I have immense gratitude for his help and guidance. If you need to land bits on a machine, Heath is one of the best people to talk to.

Claims, Disclaims

AI Assistance

I wrote this blog post with some help from AI. I read it over and edited it but some parts have been written by AI.

It’s important to be transparent about AI assistance. So much of the stuff I read now is crowded with AI turns of phrase and can be skimmed or ignored. Phrases like “It’s not just a blog post, it’s a platform for ideation. This is a step change.” are a waste of human cognition so I eliminated them where I found them.

Warranty?

No warranty. This is a personal blog. There are no guarantees about relevance, product direction, support, or anything else. It’s intended as educational information. Getting things right is an exercise for the reader who is in charge of their own success.

This post is licensed under CC BY 4.0 by the author.