# Copyright (c) .NET Foundation and contributors. All rights reserved. # Licensed under the MIT license. See LICENSE file in the project root for full license information. # # Note: This script should be compatible with the dash shell used in Ubuntu. So avoid bashisms! See https://wiki.ubuntu.com/DashAsBinSh for more info # Stop script on NZEC set -e # Stop script if unbound variable found (use ${var:-} if intentional) set -u # By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success # This is causing it to fail set -o pipefail # Use in the the functions: eval $invocation invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' # standard output may be used as a return value in the functions # we need a way to write text on the screen in the functions so that # it won't interfere with the return value. # Exposing stream 3 as a pipe to standard output of the script itself exec 3>&1 # Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. # See if stdout is a terminal if [ -t 1 ]; then # see if it supports colors ncolors=$(tput colors) if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then bold="$(tput bold)" normal="$(tput sgr0)" black="$(tput setaf 0)" red="$(tput setaf 1)" green="$(tput setaf 2)" yellow="$(tput setaf 3)" blue="$(tput setaf 4)" magenta="$(tput setaf 5)" cyan="$(tput setaf 6)" white="$(tput setaf 7)" fi fi say_err() { printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 } say() { # using stream 3 (defined in the beginning) to not interfere with stdout of functions # which may be used as return value printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 } say_verbose() { if [ "$verbose" = true ]; then say "$1" fi } get_current_os_name() { eval $invocation local uname=$(uname) if [ "$uname" = "Darwin" ]; then echo "osx" return 0 else # Detect Distro if [ "$(cat /etc/*-release | grep -cim1 ubuntu)" -eq 1 ]; then echo "ubuntu" return 0 elif [ "$(cat /etc/*-release | grep -cim1 centos)" -eq 1 ]; then echo "centos" return 0 elif [ "$(cat /etc/*-release | grep -cim1 rhel)" -eq 1 ]; then echo "rhel" return 0 elif [ "$(cat /etc/*-release | grep -cim1 debian)" -eq 1 ]; then echo "debian" return 0 fi fi say_err "OS name could not be detected" return 1 } machine_has() { eval $invocation which "$1" > /dev/null 2>&1 return $? } check_min_reqs() { if ! machine_has "curl"; then say_err "curl is required to download dotnet. Install curl to proceed." return 1 fi return 0 } check_pre_reqs() { eval $invocation local failing=false; if [ "${DOTNET_INSTALL_SKIP_PREREQS:-}" = "1" ]; then return 0 fi if [ "$(uname)" = "Linux" ]; then if ! [ -x "$(command -v ldconfig)" ]; then echo "ldconfig is not in PATH, trying /sbin/ldconfig." LDCONFIG_COMMAND="/sbin/ldconfig" else LDCONFIG_COMMAND="ldconfig" fi [ -z "$($LDCONFIG_COMMAND -p | grep libunwind)" ] && say_err "Unable to locate libunwind. Install libunwind to continue" && failing=true [ -z "$($LDCONFIG_COMMAND -p | grep libssl)" ] && say_err "Unable to locate libssl. Install libssl to continue" && failing=true [ -z "$($LDCONFIG_COMMAND -p | grep libcurl)" ] && say_err "Unable to locate libcurl. Install libcurl to continue" && failing=true [ -z "$($LDCONFIG_COMMAND -p | grep libicu)" ] && say_err "Unable to locate libicu. Install libicu to continue" && failing=true [ -z "$($LDCONFIG_COMMAND -p | grep gettext)" ] && say_err "Unable to locate gettext. Install gettext to continue" && failing=true fi if [ "$failing" = true ]; then return 1 fi return 0 } # args: # input - $1 to_lowercase() { #eval $invocation echo "$1" | tr '[:upper:]' '[:lower:]' return 0 } # args: # input - $1 remove_trailing_slash() { #eval $invocation local input=${1:-} echo "${input%/}" return 0 } # args: # input - $1 remove_beginning_slash() { #eval $invocation local input=${1:-} echo "${input#/}" return 0 } # args: # root_path - $1 # child_path - $2 - this parameter can be empty combine_paths() { eval $invocation # TODO: Consider making it work with any number of paths. For now: if [ ! -z "${3:-}" ]; then say_err "combine_paths: Function takes two parameters." return 1 fi local root_path=$(remove_trailing_slash $1) local child_path=$(remove_beginning_slash ${2:-}) say_verbose "combine_paths: root_path=$root_path" say_verbose "combine_paths: child_path=$child_path" echo "$root_path/$child_path" return 0 } get_machine_architecture() { eval $invocation # Currently the only one supported echo "x64" return 0 } # args: # architecture - $1 get_normalized_architecture_from_architecture() { eval $invocation local architecture=$(to_lowercase $1) case $architecture in \) echo "$(get_normalized_architecture_from_architecture $(get_machine_architecture))" return 0 ;; amd64|x64) echo "x64" return 0 ;; x86) say_err "Architecture ``x86`` currently not supported" return 1 ;; esac say_err "Architecture ``$architecture`` not supported. If you think this is a bug, please report it at https://github.com/dotnet/cli/issues" return 1 } # version_info is a conceptual two line string representing commit hash and 4-part version # format: # Line 1: # commit_hash # Line 2: # 4-part version # args: # version_text - stdin get_version_from_version_info() { eval $invocation cat | tail -n 1 return 0 } # args: # version_text - stdin get_commit_hash_from_version_info() { eval $invocation cat | head -n 1 return 0 } # args: # install_root - $1 # relative_path_to_package - $2 # specific_version - $3 is_dotnet_package_installed() { eval $invocation local install_root=$1 local relative_path_to_package=$2 local specific_version=${3//[$'\t\r\n']} local dotnet_package_path=$(combine_paths $(combine_paths $install_root $relative_path_to_package) $specific_version) say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" if [ -d "$dotnet_package_path" ]; then return 0 else return 1 fi } # args: # azure_feed - $1 # azure_channel - $2 # normalized_architecture - $3 get_latest_version_info() { eval $invocation local azure_feed=$1 local azure_channel=$2 local normalized_architecture=$3 local osname=$(get_current_os_name) local version_file_url="$azure_feed/$azure_channel/dnvm/latest.$osname.$normalized_architecture.version" say_verbose "get_latest_version_info: latest url: $version_file_url" download $version_file_url return $? } # args: # channel - $1 get_azure_channel_from_channel() { eval $invocation local channel=$(to_lowercase $1) case $channel in future|dev) echo "dev" return 0 ;; preview|beta) echo "beta" return 0 ;; production) say_err "Production channel does not exist yet" return 1 esac say_err "``$1`` is an invalid channel name. Use one of the following: ``future``, ``preview``, ``production``" return 1 } # args: # azure_feed - $1 # azure_channel - $2 # normalized_architecture - $3 # version - $4 get_specific_version_from_version() { eval $invocation local azure_feed=$1 local azure_channel=$2 local normalized_architecture=$3 local version=$(to_lowercase $4) case $version in latest) local version_info="$(get_latest_version_info $azure_feed $azure_channel $normalized_architecture)" say_verbose "get_specific_version_from_version: version_info=$version_info" echo "$version_info" | get_version_from_version_info return 0 ;; lkg) say_err "``--version LKG`` not supported yet." return 1 ;; *) echo $version return 0 ;; esac } # args: # azure_feed - $1 # azure_channel - $2 # normalized_architecture - $3 # specific_version - $4 construct_download_link() { eval $invocation local azure_feed=$1 local azure_channel=$2 local normalized_architecture=$3 local specific_version=${4//[$'\t\r\n']} local osname=$(get_current_os_name) local download_link="$azure_feed/$azure_channel/Binaries/$specific_version/dotnet-dev-$osname-$normalized_architecture.$specific_version.tar.gz" echo "$download_link" return 0 } get_user_share_path() { eval $invocation if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then echo $DOTNET_INSTALL_DIR else echo "$HOME/.dotnet" fi return 0 } # args: # install_dir - $1 resolve_installation_path() { eval $invocation local install_dir=$1 if [ "$install_dir" = "" ]; then local user_share_path=$(get_user_share_path) say_verbose "resolve_installation_path: share_path=$user_share_path" echo "$user_share_path" return 0 fi echo "$install_dir" return 0 } # args: # install_root - $1 get_installed_version_info() { eval $invocation local install_root=$1 local version_file=$(combine_paths "$install_root" "$local_version_file_relative_path") say_verbose "Local version file: $version_file" if [ ! -z "$version_file" ] | [ -r "$version_file" ]; then local version_info="$(cat $version_file)" echo "$version_info" return 0 fi say_verbose "Local version file not found." return 0 } # args: # relative_or_absolute_path - $1 get_absolute_path() { eval $invocation local relative_or_absolute_path=$1 echo $(cd $(dirname "$1") && pwd -P)/$(basename "$1") return 0 } # args: # input_files - stdin # root_path - $1 # out_path - $2 # override - $3 copy_files_or_dirs_from_list() { eval $invocation local root_path=$(remove_trailing_slash $1) local out_path=$(remove_trailing_slash $2) local override=$3 local override_switch=$(if [ "$override" = false ]; then printf -- "-n"; fi) cat | uniq | while read -r file_path; do local path=$(remove_beginning_slash ${file_path#$root_path}) local target=$out_path/$path if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then mkdir -p $out_path/$(dirname $path) cp -R $override_switch $root_path/$path $target fi done } # args: # zip_path - $1 # out_path - $2 extract_dotnet_package() { eval $invocation local zip_path=$1 local out_path=$2 local temp_out_path=$(mktemp -d $temporary_file_template) local failed=false tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' find $temp_out_path -type f | grep -Eo $folders_with_version_regex | copy_files_or_dirs_from_list $temp_out_path $out_path false find $temp_out_path -type f | grep -Ev $folders_with_version_regex | copy_files_or_dirs_from_list $temp_out_path $out_path true rm -rf $temp_out_path if [ "$failed" = true ]; then say_err "Extraction failed" return 1 fi } # args: # remote_path - $1 # [out_path] - $2 - stdout if not provided download() { eval $invocation local remote_path=$1 local out_path=${2:-} local failed=false if [ -z "$out_path" ]; then curl --fail -s $remote_path || failed=true else curl --fail -s -o $out_path $remote_path || failed=true fi if [ "$failed" = true ]; then say_err "Download failed" return 1 fi } calculate_vars() { eval $invocation azure_channel=$(get_azure_channel_from_channel "$channel") say_verbose "azure_channel=$azure_channel" normalized_architecture=$(get_normalized_architecture_from_architecture "$architecture") say_verbose "normalized_architecture=$normalized_architecture" specific_version=$(get_specific_version_from_version $azure_feed $azure_channel $normalized_architecture $version) say_verbose "specific_version=$specific_version" if [ -z "$specific_version" ]; then say_err "Could not get version information." return 1 fi download_link=$(construct_download_link $azure_feed $azure_channel $normalized_architecture $specific_version) say_verbose "download_link=$download_link" install_root=$(resolve_installation_path $install_dir) say_verbose "install_root=$install_root" } install_dotnet() { eval $invocation if is_dotnet_package_installed $install_root "sdk" $specific_version; then say ".NET SDK version $specific_version is already installed." return 0 fi mkdir -p $install_root zip_path=$(mktemp $temporary_file_template) say_verbose "Zip path: $zip_path" say "Downloading $download_link" download "$download_link" $zip_path say_verbose "Downloaded file exists and readable? $(if [ -r $zip_path ]; then echo "yes"; else echo "no"; fi)" say "Extracting zip" extract_dotnet_package $zip_path $install_root return 0 } local_version_file_relative_path="/.version" bin_folder_relative_path="" temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" channel="preview" version="Latest" install_dir="" architecture="" debug_symbols=false dry_run=false no_path=false azure_feed="https://dotnetcli.blob.core.windows.net/dotnet" verbose=false while [ $# -ne 0 ] do name=$1 case $name in -c|--channel|-[Cc]hannel) shift channel=$1 ;; -v|--version|-[Vv]ersion) shift version="$1" ;; -i|--install-dir|-[Ii]nstall[Dd]ir) shift install_dir="$1" ;; --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) shift architecture="$1" ;; --debug-symbols|-[Dd]ebug[Ss]ymbols) debug_symbols=true ;; --dry-run|-[Dd]ry[Rr]un) dry_run=true ;; --no-path|-[Nn]o[Pp]ath) no_path=true ;; --verbose|-[Vv]erbose) verbose=true ;; --azure-feed|-[Aa]zure[Ff]eed) shift azure_feed="$1" ;; -?|--?|-h|--help|-[Hh]elp) script_name="$(basename $0)" echo ".NET Tools Installer" echo "Usage: $script_name [-c|--channel ] [-v|--version ] [-p|--prefix ]" echo " $script_name -h|-?|--help" echo "" echo "$script_name is a simple command line interface for obtaining dotnet cli." echo "" echo "Options:" echo " -c,--channel Download from the CHANNEL specified (default: $channel)." echo " -Channel" echo " -v,--version Use specific version, ``latest`` or ``lkg``. Defaults to ``latest``." echo " -Version" echo " -i,--install-dir Install under specified location (see Install Location below)" echo " -InstallDir" echo " --architecture Architecture of .NET Tools. Currently only x64 is supported." echo " --arch,-Architecture,-Arch" echo " --debug-symbols,-DebugSymbols Specifies if symbols should be included in the installation." echo " --dry-run,-DryRun Do not perform installation. Display download link." echo " --no-path, -NoPath Do not set PATH for the current process." echo " --verbose,-Verbose Display diagnostics information." echo " --azure-feed,-AzureFeed Azure feed location. Defaults to $azure_feed" echo " -?,--?,-h,--help,-Help Shows this help message" echo "" echo "Install Location:" echo " Location is chosen in following order:" echo " - --install-dir option" echo " - Environmental variable DOTNET_INSTALL_DIR" echo " - /usr/local/share/dotnet" exit 0 ;; *) say_err "Unknown argument \`$name\`" exit 1 ;; esac shift done check_min_reqs calculate_vars if [ "$dry_run" = true ]; then say "Payload URL: $download_link" say "Repeatable invocation: ./$(basename $0) --version $specific_version --channel $channel --install-dir $install_dir" exit 0 fi check_pre_reqs install_dotnet bin_path=$(get_absolute_path $(combine_paths $install_root $bin_folder_relative_path)) if [ "$no_path" = false ]; then say "Adding to current process PATH: ``$bin_path``. Note: This change will be visible only when sourcing script." export PATH=$PATH:$bin_path else say "Binaries of dotnet can be found in $bin_path" fi say "Installation finished successfuly."