#!/bin/sh # # Copyright (c) 2018, Julian Ospald # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ########################## #--[ Global Variables ]--# ########################## # @VARIABLE: VERSION # @DESCRIPTION: # Version of this script. VERSION=0.0.7 # @VARIABLE: SCRIPT # @DESCRIPTION: # Name of this script. This will be the # shell name if this script is sourced, so # only rely on this for echos and trivial things. SCRIPT="$(basename "$0")" # @VARIABLE: VERBOSE # @DESCRIPTION: # Whether to print verbose messages in this script. VERBOSE=false # @VARIABLE: FORCE # @DESCRIPTION: # Whether to force installation and overwrite files. FORCE=false # @VARIABLE: GHCUP_INSTALL_BASE_PREFIX # @DESCRIPTION: # The main install directory prefix, under which .ghcup # directory will be created. This directory is user # configurable via the environment variable of the # same name. It must be non-empty and the path # it points to must exist. : "${GHCUP_INSTALL_BASE_PREFIX:=$HOME}" # @VARIABLE: INSTALL_BASE # @DESCRIPTION: # The main install directory where all ghcup stuff happens. INSTALL_BASE="$GHCUP_INSTALL_BASE_PREFIX/.ghcup" # @VARIABLE: GHC_LOCATION # @DESCRIPTION: # The location where ghcup will install different ghc versions. # This is expected to be a subdirectory of INSTALL_BASE. GHC_LOCATION="$INSTALL_BASE/ghc" # @VARIABLE: BIN_LOCATION # @DESCRIPTION: # The location where ghcup will create symlinks for GHC binaries. # This is expected to be a subdirectory of INSTALL_BASE. BIN_LOCATION="$INSTALL_BASE/bin" # @VARIABLE: CACHE_LOCATION # @DESCRIPTION: # The location where ghcup will put tarballs for caching. # This is expected to be a subdirectory of INSTALL_BASE. CACHE_LOCATION="$INSTALL_BASE/cache" # @VARIABLE: DOWNLOADER # @DESCRIPTION: # What program to use for downloading files. DOWNLOADER="curl" # @VARIABLE: DOWNLOADER_OPTS # @DESCRIPTION: # Options passed to the download program. DOWNLOADER_OPTS="-L --fail -O" # @VARIABLE: DOWNLOADER_STDOUT_OPTS # @DESCRIPTION: # Options passed to the download program when printing the content to stdout. DOWNLOADER_STDOUT_OPTS="-L --fail" # @VARIABLE: GHC_DOWNLOAD_BASEURL # @DESCRIPTION: # Base URL for all GHC tarballs. GHC_DOWNLOAD_BASEURL="https://downloads.haskell.org/~ghc" # @VARIABLE: JOBS # @DESCRIPTION: # How many jobs to use for compiling GHC. JOBS="1" # @VARIABLE: SOURCE # @DESCRIPTION: # The $0 argument, which contains # the script name. SOURCE="$0" # @VARIABLE: BASE_DOWNLOAD_URL # DESCRIPTION: # The base url for downloading stuff like meta files, requirements files etc. BASE_DOWNLOAD_URL="https://gitlab.haskell.org/haskell/ghcup/raw/master/" # @VARIABLE: SCRIPT_UPDATE_URL # @DESCRIPTION: # Location to update this script from. SCRIPT_UPDATE_URL="${BASE_DOWNLOAD_URL}/ghcup" # @VARIABLE: META_DOWNLOAD_URL # DESCRIPTION: # The url of the meta file for getting # download information for ghc/cabal-install etc. META_DOWNLOAD_URL="${GHCUP_META_DOWNLOAD_URL:=${BASE_DOWNLOAD_URL}/.download-urls}" # @VARIABLE: META_DOWNLOAD_FORMAT # DESCRIPTION: # The version of the meta file format. # This determines whether this script can read the # file from "${META_DOWNLOAD_URL}". META_DOWNLOAD_FORMAT="1" # @VARIABLE: META_VERSION_URL # DESCRIPTION: # The url of the meta file for getting # available versions for ghc/cabal-install etc. META_VERSION_URL="${GHCUP_META_VERSION_URL:=${BASE_DOWNLOAD_URL}/.available-versions}" # @VARIABLE: META_VERSION_FORMAT # DESCRIPTION: # The version of the meta file format. # This determines whether this script can read the # file from "${META_VERSION_URL}". META_VERSION_FORMAT="1" # @VARIABLE: BUG_URL # DESCRIPTION: # The url to report bugs to. BUG_URL="https://gitlab.haskell.org/haskell/ghcup/issues" # @VARIABLE: CACHING # @DESCRIPTION: # Whether to cache tarballs in $CACHE_LOCATION. CACHING=false #################### #--[ Print Help ]--# #################### # @FUNCTION: usage # @DESCRIPTION: # Print the help message for 'ghcup' to STDERR # and exit the script with status code 1. usage() { (>&2 echo "ghcup ${VERSION} GHC up toolchain installer USAGE: ${SCRIPT} [FLAGS] FLAGS: -v, --verbose Enable verbose output -h, --help Prints help information -V, --version Prints version information -w, --wget Use wget instead of curl -c, --cache Use \"${CACHE_LOCATION}\" for caching tarballs (these will not be removed by ghcup) SUBCOMMANDS: install Install GHC$(${VERBOSE} && printf "\n compile Compile and install GHC from source (UNSTABLE!!!)") set Set currently active GHC version list Show available GHCs and other tools upgrade Upgrade this script in-place rm Remove an already installed GHC install-cabal Install cabal-install debug-info Print debug info (e.g. detected system/distro) changelog Show the changelog of a GHC release (online) print-system-reqs Print an approximation of system requirements DISCUSSION: ghcup installs the Glasgow Haskell Compiler from the official release channels, enabling you to easily switch between different versions. ") exit 1 } # @FUNCTION: install_usage # @DESCRIPTION: # Print the help message for 'ghcup install' to STDERR # and exit the script with status code 1. install_usage() { (>&2 echo "ghcup-install Install GHC from binary tarball USAGE: ${SCRIPT} install [FLAGS] [VERSION|TAG] FLAGS: -h, --help Prints help information -f, --force Overwrite already existing installation ARGS: [VERSION|TAG] E.g. \"8.4.3\" or \"8.6.1\" or a tag like \"recommended\" or \"latest\" (default: discovers recommended version) DISCUSSION: Installs the specified GHC version (or a recommended default one) into a self-contained \"~/.ghcup/ghc/\" directory and symlinks the ghc binaries to \"~/.ghcup/bin/-\". ") exit 1 } # @FUNCTION: set_usage # @DESCRIPTION: # Print the help message for 'ghcup set' to STDERR # and exit the script with status code 1. set_usage() { (>&2 echo "ghcup-set Set the currently active GHC to the specified version USAGE: ${SCRIPT} set [FLAGS] [VERSION|TAG] FLAGS: -h, --help Prints help information ARGS: [VERSION|TAG] E.g. \"8.4.3\" or \"8.6.3\" or a tag like \"recommended\" or \"latest\" (default: discovers recommended version) DISCUSSION: Sets the the current GHC version by creating non-versioned symlinks for all ghc binaries of the specified version in \"~/.ghcup/bin/\". ") exit 1 } # @FUNCTION: upgrade_usage # @DESCRIPTION: # Print the help message for 'ghcup upgrade' to STDERR # and exit the script with status code 1. upgrade_usage() { (>&2 echo "ghcup-upgrade Update the ghcup script in-place USAGE: ${SCRIPT} upgrade [FLAGS] [TARGET-LOCATION] FLAGS: -i, --inplace Update this script in-place (wherever it's at) -h, --help Prints help information ARGS: [TARGET-LOCATION] Where to place the updated script (defaults to ${BIN_LOCATION}). This is ignored if --inplace is issued as well. ") exit 1 } # @FUNCTION: rm_usage # @DESCRIPTION: # Print the help message for 'ghcup rm' to STDERR # and exit the script with status code 1. rm_usage() { (>&2 echo "ghcup-rm Remove the given GHC version installed by ghcup USAGE: ${SCRIPT} rm [FLAGS] FLAGS: -h, --help Prints help information -f, --force Don't prompt user ARGS: E.g. \"8.4.3\" or \"8.6.1\" ") exit 1 } # @FUNCTION: install_cabal_usage # @DESCRIPTION: # Print the help message for 'ghcup install-cabal' to STDERR # and exit the script with status code 1. install_cabal_usage() { (>&2 echo "ghcup-install-cabal Install the specified or a default cabal version USAGE: ${SCRIPT} install-cabal [FLAGS] [VERSION|TAG] FLAGS: -h, --help Prints help information ARGS: [VERSION|TAG] E.g. \"2.4.0.0\" or a tag like \"recommended\" or \"latest\" DISCUSSION: Installs the specified cabal-install version (or the default recommended) into \"${BIN_LOCATION}\", so it can be overwritten by later \"cabal new-install cabal-install\", which installs into \"~/.cabal/bin\". Make sure to set up your PATH appropriately, so the cabal installation takes precedence. ") exit 1 } # @FUNCTION: compile_usage # @DESCRIPTION: # Print the help message for 'ghcup compile' to STDERR # and exit the script with status code 1. compile_usage() { (>&2 echo "ghcup-compile Compile and install the specified GHC version USAGE: ${SCRIPT} compile [FLAGS] FLAGS: -h, --help Prints help information -f, --force Overwrite already existing installation -j, --jobs How many jobs for compilation -c, --build-config Use the given config file as build config ARGS: E.g. \"8.4.3\" or \"8.6.1\" E.g. \"ghc-8.2.2\" or a full path DISCUSSION: Compiles and installs the specified GHC version into a self-contained \"~/.ghcup/ghc/\" directory and symlinks the ghc binaries to \"~/.ghcup/bin/-\". EXAMPLE: ghcup -v compile -f -j 4 8.4.2 ghc-8.2.2 ") exit 1 } # @FUNCTION: debug_info_usage # @DESCRIPTION: # Print the help message for 'ghcup debug-info' to STDERR # and exit the script with status code 1. debug_info_usage() { (>&2 echo "ghcup-debug-info Print debug info (e.g. detected system/distro) USAGE: ${SCRIPT} debug-info FLAGS: -h, --help Prints help information DISCUSSION: Prints debug information, e.g. detected system architecture, distribution, version, as well as script variables. This is mainly useful for debugging purposes. ") exit 1 } # @FUNCTION: list_usage # @DESCRIPTION: # Print the help message for 'ghcup list' to STDERR # and exit the script with status code 1. list_usage() { (>&2 echo "ghcup-list Show available GHCs and other tools USAGE: ${SCRIPT} list FLAGS: -h, --help Prints help information -t, --tool Tool to list versions for. Default is ghc only. -c, --show-criteria Show only installed or set tool versions -r, --raw-format Raw format, for machine parsing DISCUSSION: Prints tools (e.g. GHC and cabal-install) and their available/installed/set versions. ") exit 1 } # @FUNCTION: changelog_usage # @DESCRIPTION: # Print the help message for 'ghcup changelog' to STDERR # and exit the script with status code 1. changelog_usage() { (>&2 echo "ghcup-changelog View the online changelog for the given GHC version USAGE: ${SCRIPT} changelog [FLAGS] [VERSION|TAG] FLAGS: -h, --help Prints help information ARGS: [VERSION|TAG] E.g. \"8.4.3\" or \"8.6.3\" or a tag like \"recommended\" or \"latest\" (default: discovers latest version) DISCUSSION: Opens the online changelog for the given GHC version via xdg-open. ") exit 1 } # @FUNCTION: print_system_reqs_usage # @DESCRIPTION: # Print the help message for 'ghcup print-system-reqs' to STDERR # and exit the script with status code 1. print_system_reqs_usage() { (>&2 echo "ghcup-print-system-reqs Print an approximation of system requirements USAGE: ${SCRIPT} print-system-reqs FLAGS: -h, --help Prints help information DISCUSSION: Just prints an approximation of the system requirements for the 'recommended' GHC version and the 'latest' distro version you are on. Review this output carefully! ") exit 1 } ########################### #--[ Utility functions ]--# ########################### # @FUNCTION: die # @USAGE: [msg] # @DESCRIPTION: # Exits the shell script with status code 2 # and prints the given message in red to STDERR, if any. die() { (>&2 red_message "$1") exit 2 } # @FUNCTION: edo # @USAGE: # @DESCRIPTION: # Executes the given command. Also prints what # command that is (in blue) if verbosity is enabled. # Exits with status code 2 if the command failed. edo() { if ${VERBOSE} ; then printf "\\033[0;34m%s\\033[0m\\n" "$*" 1>&2 fi "$@" || exit 2 } # @FUNCTION: emake # @USAGE: [arguments] # @DESCRIPTION: # Wrapper around 'make', may call 'gmake' if it exists. emake() { # avoid re-checking for gmake if [ -n "${MAKE}" ] ; then # shellcheck disable=SC2086 edo ${MAKE} "$@" else if command_exists gmake ; then MAKE="gmake" # shellcheck disable=SC2086 edo ${MAKE} "$@" else MAKE="make" # shellcheck disable=SC2086 edo ${MAKE} "$@" fi fi } # @FUNCTION: debug_message # @USAGE: # @DESCRIPTION: # Print a blue debug message if verbosity is enabled. debug_message() { if ${VERBOSE} ; then (>&2 printf "\\033[0;34m%s\\033[0m\\n" "$1") fi } # @FUNCTION: optionv # @USAGE: [arg2] # @DESCRIPTION: # If verbosity is enabled, echo the first argument, otherwise # the second (if any). # @STDOUT: first or second argument optionv() { if ${VERBOSE} ; then echo "$1" else if [ -n "$2" ] ; then echo "$2" fi fi } # @FUNCTION: status_message # @USAGE: # @DESCRIPTION: # Print a green status message. status_message() { printf "\\033[0;32m%s\\033[0m\\n" "$1" } # @FUNCTION: warning_message # @USAGE: # @DESCRIPTION: # Print a yellow warning message. warning_message() { printf "\\033[1;33m%s\\033[0m\\n" "$1" } # @FUNCTION: red_message # @USAGE: # @DESCRIPTION: # Print a red message. red_message() { printf "\\033[0;31m%s\\033[0m\\n" "$1" } # @FUNCTION: command_exists # @USAGE: # @DESCRIPTION: # Check if a command exists (no arguments). # @RETURNS: 0 if the command exists, non-zero otherwise command_exists() { [ -z "$1" ] && die "Internal error: no argument given to command_exists" command -V "$1" >/dev/null 2>&1 return $? } # @FUNCTION: check_required_commands # @USAGE: [additional-commands] # @DESCRIPTION: # Check that all required commands for this script exist. # @STDOUT: The commands that do not exist # @RETURNS: 0 if all command exists, non-zero otherwise check_required_commands() { _missing_commands= mydistro=$(get_distro_alias "$(get_distro_name)") for com in "$@" awk uname basename tar gzip mktemp dirname ; do command_exists "${com}" || { _missing_commands="${_missing_commands} ${com}" } done unset com # darwin uses tar's built-in xz decompression if test "${mydistro}" != "darwin"; then command_exists xz || { _missing_commands="${_missing_commands} xz" } fi if [ -n "${_missing_commands}" ] ; then printf "%s" "${_missing_commands}" unset _missing_commands mydistro return 1 else unset _missing_commands mydistro return 0 fi } # @FUNCTION: get_distro_name # @DESCRIPTION: # Gets the current distro identifier following # https://unix.stackexchange.com/a/6348 # (see also http://linuxmafia.com/faq/Admin/release-files.html) # @STDOUT: current distro identifier get_distro_name() { if [ -f /etc/os-release ]; then # freedesktop.org and systemd # shellcheck disable=SC1091 . /etc/os-release printf "%s" "$NAME" elif command_exists lsb_release ; then # linuxbase.org printf "%s" "$(lsb_release -si)" elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command # shellcheck disable=SC1091 . /etc/lsb-release printf "%s" "$DISTRIB_ID" elif [ -f /etc/redhat-release ]; then case "$(cat /etc/redhat-release)" in # Older CentOS releases didn't have a /etc/centos-release file "CentOS release "*) printf "CentOS" ;; "CentOS Linux release "*) printf "CentOS Linux" ;; "Fedora release "*) printf "Fedora" ;; # Fallback to uname *) printf "%s" "$(uname -s)" ;; esac elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. printf "Debian" else # Fall back to uname, e.g. "Linux ", also works for BSD, etc. printf "%s" "$(uname -s)" fi } # @FUNCTION: get_distro_ver # @DESCRIPTION: # Gets the current distro version (if any) following # https://unix.stackexchange.com/a/6348 # @STDOUT: current distro version, if any get_distro_ver() { if [ -f /etc/os-release ]; then # freedesktop.org and systemd # shellcheck disable=SC1091 . /etc/os-release printf "%s" "$VERSION_ID" elif command_exists lsb_release ; then # linuxbase.org printf "%s" "$(lsb_release -sr)" elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command # shellcheck disable=SC1091 . /etc/lsb-release printf "%s" "$DISTRIB_RELEASE" elif [ -f /etc/redhat-release ]; then case "$(cat /etc/redhat-release)" in # NB: Older CentOS releases didn't have a /etc/centos-release file "CentOS release "*|"Fedora release "*) printf "%s" "$(awk 'NR==1 { split($3, a, "."); print a[1] }' /etc/redhat-release)" ;; "CentOS Linux release "*) printf "%s" "$(awk 'NR==1 { split($4, a, "."); print a[1] }' /etc/redhat-release)" ;; # Fallback to uname *) printf "%s" "$(uname -r)" ;; esac elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. printf "%s" "$(cat /etc/debian_version)" else case "$(uname -s)" in AIX) printf "%s" "$(uname -v)" ;; FreeBSD) # we only care about the major numeric version part left of # the '.' in "11.2-RELEASE". printf "%s" "$(uname -r | cut -d . -f 1)" ;; *) # Fall back to uname, e.g. "Linux ", also works for BSD, etc. printf "%s" "$(uname -r)" esac fi } # @FUNCTION: get_arch # @DESCRIPTION: # Gets the architecture following # https://unix.stackexchange.com/a/6348 # Fails for any architecture that we don't know a GHC version for. # @STDOUT: current architecture get_arch() { myarch=$(uname -m) case "${myarch}" in x86_64|amd64) printf "x86_64" # or AMD64 or Intel64 or whatever ;; i*86) printf "i386" # or IA32 or Intel32 or whatever ;; *) case "$(uname -s)" in AIX) case "$(uname -p)" in powerpc) printf "powerpc" ;; *) die "Cannot figure out architecture on AIX (was: ${myarch})" ;; esac ;; *) die "Cannot figure out architecture (was: ${myarch})" ;; esac esac unset myarch } # @FUNCTION: try_download_url # @USAGE: # @DESCRIPTION: # Tries to get the download url of a tool with our # specified format for download urls (see ${META_DOWNLOAD_URL}"). # STDOUT: the download url, if an appropriate was found try_download_url() { [ "$#" -lt 5 ] && die "Internal error: not enough arguments to try_download_url" tool=$1 ver=$2 arch=$3 distro_ident=$4 filename=$5 awk " NF { split(\$4,a,\",\") if (\$1 == \"${tool}\" && \$2 == \"${ver}\" && \$3 == \"${arch}\") { for (i in a) if (a[i] == \"${distro_ident}\") { print \$5 exit } } }" "${filename}" || die "awk failed!" unset tool ver arch distro_ident filename } # @FUNCTION: check_meta_file_version # @USAGE: # @DESCRIPTION: # Check that the given meta file has the same format version # as specified, otherwise die. check_meta_file_version() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to check_meta_file_version" mymetavar=$(awk " NR==1 { if (\$2 ~ \"fmt-version\") { { split(\$2,a,\"=\") print a[2] exit } } }" "$1") if [ "${mymetavar}" != "$2" ] ; then die "Unsupported meta file format, run: ${SCRIPT} upgrade" fi unset mymetavar } # @FUNCTION: get_download_url # @USAGE: # @DESCRIPTION: # Gets the download url for the given tool and version # and the current distro and architecture (which it tries to discover). # This uses "${META_DOWNLOAD_URL}" for url discovery. # @STDOUT: download url or nothing if no appropriate was found get_download_url() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to get_download_url" mytool=$1 myver=$2 myarch=$(get_arch) [ -z "${myarch}" ] && die "failed to get architecture" mydistro=$(get_distro_alias "$(get_distro_name)") mydistrover=$(get_distro_ver) meta_file="$(get_meta_download_file)" [ -z "${meta_file}" ] && die "failed to get meta file" # 1st try with full distro=ver url=$(try_download_url "${mytool}" "${myver}" "${myarch}" "${mydistro}=${mydistrover}" "${meta_file}") if [ -n "${url}" ] ; then printf "%s" "${url}" exit 0 fi # 2nd try with just distro url=$(try_download_url "${mytool}" "${myver}" "${myarch}" "${mydistro}" "${meta_file}") if [ -n "${url}" ] ; then printf "%s" "${url}" exit 0 fi # 3rd try with unknown url=$(try_download_url "${mytool}" "${myver}" "${myarch}" "unknown" "${meta_file}") if [ -n "${url}" ] ; then printf "%s" "${url}" exit 0 fi unset mytool myver myarch mydistro mydistrover meta_file } # @FUNCTION: get_tool_ver_from_tag # @USAGE: # @DESCRIPTION: # Gets the tool version with the given tag (first match) from # "${META_VERSION_URL}". # STDOUT: the version, if any, or nothing get_tool_ver_from_tag() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to get_tool_ver_from_tag" mytool=$1 mytag=$2 meta_file="$(get_meta_version_file)" [ -z "${meta_file}" ] && die "failed to get meta file" awk " NF { if (\$1 == \"${mytool}\") { split(\$3,a,\",\"); for (i in a) if (a[i] == \"${mytag}\") { print \$2 exit } } }" "${meta_file}" || die "awk failed!" unset mytool mytag meta_file } # @FUNCTION: ghc_already_installed # @USAGE: # @DESCRIPTION: # Checks whether the specified GHC version # has been installed by ghcup already. # @RETURN: 0 if GHC is already installed, 1 otherwise ghc_already_installed() { [ -z "$1" ] && die "Internal error: no argument given to ghc_already_installed" if [ -e "$(get_ghc_location "$1")" ] ; then return 0 else return 1 fi } # @FUNCTION: cabal_already_installed # @USAGE: # @DESCRIPTION: # Checks whether the specified cabal version # has been installed by ghcup already. # @RETURN: 0 if cabal is already installed, 1 otherwise cabal_already_installed() { [ -z "$1" ] && die "Internal error: no argument given to cabal_already_installed" if [ -x "${BIN_LOCATION}/cabal" ] ; then if [ "$("${BIN_LOCATION}/cabal" --numeric-version)" = "$1" ] ; then return 0 else return 1 fi else return 1 fi } # @FUNCTION: tool_already_installed # @USAGE: # @DESCRIPTION: # Checks whether the specified tool and version # has been installed by ghcup already. # @RETURN: 0 if tool is already installed, 1 otherwise tool_already_installed() { if [ "$1" = "ghc" ] ; then ghc_already_installed "$2" return $? elif [ "$1" = "cabal-install" ] ; then cabal_already_installed "$2" return $? else return 1 fi } # @FUNCTION: get_ghc_location # @USAGE: # @DESCRIPTION: # Gets/prints the location where the specified GHC is or would be installed. # Doesn't check whether that directory actually exist. Use # 'ghc_already_installed' for that. # @STDOUT: ghc location get_ghc_location() { [ -z "$1" ] && die "Internal error: no argument given to get_ghc_location" myghcver=$1 inst_location=${GHC_LOCATION}/${myghcver} printf "%s" "${inst_location}" unset myghcver inst_location } # @FUNCTION: download # @USAGE: # @DESCRIPTION: # Downloads the given url as a file into the current directory. download() { [ -z "$1" ] && die "Internal error: no argument given to download" # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_OPTS} "$1" } # @FUNCTION: download_to_cache # @USAGE: # @DESCRIPTION: # Downloads the given url as a file into the cache directory # and makes sure the file is deleted on failed/interrupted download. download_to_cache() { [ -z "$1" ] && die "Internal error: no argument given to download_to_cache" _dtc_download_url="$1" _dtc_download_tarball_name=$(basename "${_dtc_download_url}") rm_tarball() { if [ -e "${CACHE_LOCATION}/${_dtc_download_tarball_name}" ] ; then rm "${CACHE_LOCATION}/${_dtc_download_tarball_name}" fi } ( trap 'rm_tarball' 2 edo cd "${CACHE_LOCATION}" # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_OPTS} "${_dtc_download_url}" trap - 2 ) || { rm_tarball die "Failed to download" } unset _dtc_download_tarball_name _dtc_download_url } # @FUNCTION: download_silent # @USAGE: # @DESCRIPTION: # Downloads the given url as a file into the current directory, silent, unless # verbosity is on. download_silent() { [ -z "$1" ] && die "Internal error: no argument given to download" if ${VERBOSE} ; then # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_OPTS} "$1" else # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_OPTS} "$1" 1> /dev/null 2> /dev/null fi } # @FUNCTION: download_to_stdout # @USAGE: # @DESCRIPTION: # Downloads the given url to stdout. download_to_stdout() { [ -z "$1" ] && die "Internal error: no argument given to download" # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_STDOUT_OPTS} "$1" 2> /dev/null } # @FUNCTION: unpack # @USAGE: # @DESCRIPTION: # Uncompresses and unpacks the given tarball if needed by discovering the # file extension. unpack() { [ -z "$1" ] && die "Internal error: no argument given to unpack" mydistro=$(get_distro_alias "$(get_distro_name)") filename=$1 file_ext=${filename##*.} # this is for portability, since not all # distros have tar with compression detection # capability case "${file_ext}" in xz) if test "${mydistro}" = "darwin"; then debug_message "tar xf \"${filename}\"" ( tar xf "${filename}" ) || die "unpacking failed!" else debug_message "xz -cd \"${filename}\" | tar -xf -" ( xz -cd "${filename}" | tar xf - ; ) || die "unpacking failed!" fi ;; gz) debug_message "gzip -cd \"${filename}\" | tar -xf -" ( gzip -cd "${filename}" | tar xf - ; ) || die "unpacking failed!" ;; tar) edo tar xf "${filename}" ;; *) die "Unknown file extension: \"${file_ext}\"" esac unset mydistro filename file_ext } # @FUNCTION: ask_for_confirmation # @USAGE: [confirmation-msg] # @DESCRIPTION: # Asks the user for confirmation and returns 0 for yes, 1 for no. # @RETURN: 0 if user confirmed, 1 otherwise ask_for_confirmation() { confirmation_msg=$1 if [ -n "${confirmation_msg}" ] ; then printf "%s\\n(y/n and press Enter)\\n" "${confirmation_msg}" else printf "Confirm action: (y/n and press Enter)\\n" fi read -r answer if [ "${answer}" != "${answer#[Yy]}" ] ;then return 0 else return 1 fi unset confirmation_msg answer } # @FUNCTION: get_distro_alias # @USAGE: # @DESCRIPTION: # For a given known distro name, return our internal # unique distro alias. E.g.: # Debian GNU/Linux -> debian # STDOUT: our internal distro alias get_distro_alias() { distro_name=$1 distro_alias=unknown case "${distro_name}" in "Debian"|"Debian GNU/Linux"|"debian") distro_alias=debian ;; "Ubuntu"|"ubuntu") distro_alias=ubuntu ;; "Exherbo"|"exherbo") distro_alias=exherbo ;; "Fedora"|"fedora") distro_alias=fedora ;; "CentOS Linux"|"CentOS"|"centos"|"Red Hat Enterprise Linux"*) distro_alias=centos ;; "Alpine Linux"|"Alpine") distro_alias=alpine ;; "Linux Mint"|"LinuxMint") distro_alias=mint ;; "Amazon Linux AMI") distro_alias=amazonlinux ;; "AIX") distro_alias=aix ;; "FreeBSD") distro_alias=freebsd ;; "Darwin") distro_alias=darwin ;; esac printf "%s" "${distro_alias}" unset distro_name distro_alias } # @FUNCTION: posix_realpath # @USAGE: # @DESCRIPTION: # Portably gets the realpath and prints it to stdout. # This was initially inspired by # https://gist.github.com/tvlooy/cbfbdb111a4ebad8b93e # and # https://stackoverflow.com/a/246128 # # If the file does not exist, just prints it appended to the current directory. # @STDOUT: realpath of the given file posix_realpath() { [ -z "$1" ] && die "Internal error: no argument given to posix_realpath" current_loop=0 max_loops=50 mysource=$1 while [ -h "${mysource}" ]; do current_loop=$((current_loop+1)) mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )" mysource="$(readlink "${mysource}")" [ "${mysource%${mysource#?}}"x != '/x' ] && mysource="${mydir}/${mysource}" if [ ${current_loop} -gt ${max_loops} ] ; then (>&2 echo "${1}: Too many levels of symbolic links") break fi done mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )" # TODO: better distinguish between "does not exist" and "permission denied" if [ -z "${mydir}" ] ; then (>&2 echo "${1}: Permission denied") else echo "${mydir%/}/$(basename "${mysource}")" fi unset current_loop max_loops mysource mydir } # @FUNCTION: get_meta_version_file # @DESCRIPTION: # Downloads the META_VERSION_URL # in case it hasn't been downloaded # during the execution of this script yet # and checks the format version matches # the expected one. # @STDOUT: file location get_meta_version_file() { meta_file_name="$(basename "${META_VERSION_URL}")" meta_filepath="${CACHE_LOCATION}/${meta_file_name}" if [ ! -e "${meta_filepath}" ] ; then ( edo cd "${CACHE_LOCATION}" download_silent "${META_VERSION_URL}" ) || die "downloading failed" fi check_meta_file_version "${meta_filepath}" "${META_VERSION_FORMAT}" printf "%s" "${meta_filepath}" unset meta_file_name meta_filepath } # @FUNCTION: get_meta_download_file # @DESCRIPTION: # Downloads the META_DOWNLOAD_URL # in case it hasn't been downloaded # during the execution of this script yet # and checks the format version matches # the expected one. # @STDOUT: file location get_meta_download_file() { meta_file_name="$(basename "${META_DOWNLOAD_URL}")" meta_filepath="${CACHE_LOCATION}/${meta_file_name}" if [ ! -e "${meta_filepath}" ] ; then ( edo cd "${CACHE_LOCATION}" download_silent "${META_DOWNLOAD_URL}" ) || die "downloading failed!" fi check_meta_file_version "${meta_filepath}" "${META_DOWNLOAD_FORMAT}" printf "%s" "${CACHE_LOCATION}/${meta_file_name}" unset meta_file_name meta_filepath } # @FUNCTION: known_tool_versions # @USAGE: # @DESCRIPTION: # Prints the known tool versions from # META_VERSION_URL. # @STDOUT: known ghc versions known_tool_versions() { [ -z "$1" ] && die "Internal error: no argument given to posix_realpath" mytool=$1 meta_file="$(get_meta_version_file)" [ -z "${meta_file}" ] && die "failed to get meta file" awk " NF { if (\$1 == \"${mytool}\") { print \$2 } }" "${meta_file}" || die "awk failed!" unset mytool meta_file } # @FUNCTION: known_tool_tags # @USAGE: # @DESCRIPTION: # Prints the known tool tags from # META_VERSION_URL. # @STDOUT: known tool tags known_tool_tags() { [ -z "$1" ] && die "Internal error: no argument given to known_tool_tags" mytool=$1 meta_file="$(get_meta_version_file)" [ -z "${meta_file}" ] && die "failed to get meta file" awk " NF { if (\$1 == \"${mytool}\") { split(\$3,a,\",\"); for (i in a) { print a[i] } } }" "${meta_file}" | sort -u || die "awk failed!" unset mytool meta_file } # @FUNCTION: array_contains # @USAGE: # @DESCRIPTION: # Checks whether the given elements # is in the array. # @RETURNS: returns 0 if the element is in the array, 1 otherwise array_contains() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to array_contains" element=$1 array=$2 for e in ${array} ; do if [ "${e}" = "${element}" ] ; then unset e element array return 0 fi done unset e element array return 1 } # @FUNCTION: show_ghc_installed # @DESCRIPTION: # Prints the currently selected GHC only as version string. # @STDOUT: current GHC version show_ghc_installed() { current_ghc="${BIN_LOCATION}/ghc" real_ghc=$(posix_realpath "${current_ghc}") if [ -L "${current_ghc}" ] ; then # is symlink if [ -e "${real_ghc}" ] ; then # exists (posix_realpath was called) real_ghc="$(basename "${real_ghc}" | sed 's#ghc-##')" printf "%s" "${real_ghc}" else # is a broken symlink red_message "broken symlink" fi fi unset real_ghc current_ghc } # @FUNCTION: show_cabal_installed # @DESCRIPTION: # Prints the currently selected cabal only as version string. # @STDOUT: current cabal version show_cabal_installed() { if [ -x "${BIN_LOCATION}/cabal" ] ; then "${BIN_LOCATION}/cabal" --numeric-version fi } # @FUNCTION: get_full_ghc_ver # @USAGE: # @DESCRIPTION: # Get the latest full GHC version . get_full_ghc_ver() { [ -z "$1" ] && die "Internal error: no argument given to get_full_ghc_ver" mymajorghcver=$1 latest_ghc=0 for current_ghc in "${BIN_LOCATION}/ghc-${mymajorghcver}."* ; do [ -e "${current_ghc}" ] || break real_ghc=$(posix_realpath "${current_ghc}") real_ghc="$(basename "${real_ghc}" | sed 's#ghc-##')" if [ "$(expr "${real_ghc}" \> "${latest_ghc}")" = 1 ] ; then latest_ghc=${real_ghc} fi done if [ "${latest_ghc}" != 0 ] ; then printf "%s" "${latest_ghc}" fi unset mymajorghcver latest_ghc real_ghc current_ghc } # @FUNCTION: set_ghc_major # @USAGE: # @DESCRIPTION: # Sets a ghc-x.y major version to the latest ghc-x.y.z if any is installed. set_ghc_major() { [ -z "$1" ] && die "Internal error: no argument given to set_ghc_major" full_ghc_ver="$(get_full_ghc_ver "${1%.*}")" if [ -z "${full_ghc_ver}" ] ; then die "Could not set GHC major symlink" fi set_ghc "${full_ghc_ver}" "-${1%.*}" unset full_ghc_ver } ############################ #--[ Subcommand install ]--# ############################ # @FUNCTION: install_ghc # @USAGE: # @DESCRIPTION: # Installs the given ghc version with a lot of side effects. install_ghc() { [ -z "$1" ] && die "Internal error: no argument given to install_ghc" myghcver=$1 inst_location=$(get_ghc_location "$1") [ -z "${inst_location}" ] && die "failed to get install location" download_url=$(get_download_url "ghc" "${myghcver}") if [ -z "${download_url}" ] ; then die "Could not find an appropriate download for the requested GHC-${myghcver} on your system! Please report a bug at ${BUG_URL}" fi download_tarball_name=$(basename "${download_url}") first_install=true status_message "Installing GHC-${myghcver} for $(get_distro_name) on architecture $(get_arch)" if ghc_already_installed "${myghcver}" ; then if ${FORCE} ; then echo "GHC already installed in ${inst_location}, overwriting!" else warning_message "GHC already installed in ${inst_location}, use --force to overwrite" exit 0 fi first_install=false fi tmp_dir=$(mktemp -d -t ghcup.XXXXXXXX) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( if ${CACHING} ; then if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then download_to_cache "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi edo cd "./ghc-${myghcver}" debug_message "Installing GHC into ${inst_location}" edo ./configure --prefix="${inst_location}" emake install # clean up edo cd .. [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" ) || { [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" if ${first_install} ; then [ -e "${inst_location}" ] && rm -r "${inst_location}" else warning_message "GHC force installation failed. The install might be broken." warning_message "Consider running: ghcup rm ${myghcver}" fi die "Failed to install, consider updating this script via: ${SCRIPT} upgrade" } for f in "${inst_location}"/bin/*-"${myghcver}" ; do [ -e "${f}" ] || die "Something went wrong, ${f} does not exist!" fn=$(basename "${f}") # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}/bin/${fn}" "${BIN_LOCATION}/${fn}" unset fn done # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/runhaskell "${BIN_LOCATION}/runhaskell-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hsc2hs "${BIN_LOCATION}/hsc2hs-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hp2ps "${BIN_LOCATION}/hp2ps-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hpc "${BIN_LOCATION}/hpc-${myghcver}" if [ -e "${inst_location}/bin/haddock" ] ; then # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/haddock "${BIN_LOCATION}/haddock-${myghcver}" fi status_message "Done installing, run \"ghci-${myghcver}\" or set up your current GHC via: ${SCRIPT} set ${myghcver}" unset inst_location f download_url download_tarball_name first_install tmp_dir set_ghc_major "${myghcver}" unset myghcver } ######################## #--[ Subcommand set ]--# ######################## # @FUNCTION: set_ghc # @USAGE: [target-suffix] # @DESCRIPTION: # Sets the current ghc version by creating symlinks. set_ghc() { [ -z "$1" ] && die "Internal error: no argument given to set_ghc" myghcver=$1 target_suffix=$2 inst_location=$(get_ghc_location "$1") [ -z "${inst_location}" ] && die "failed to get install location" [ -e "${inst_location}" ] || die "GHC ${myghcver} not installed yet, use: ${SCRIPT} install ${myghcver}" status_message "Setting GHC to ${myghcver}" for f in "${inst_location}"/bin/*-"${myghcver}" ; do [ -e "${f}" ] || die "Something went wrong, ${f} does not exist!" source_fn=$(basename "${f}") target_fn="$(echo "${source_fn}" | sed "s#-${myghcver}##")${target_suffix}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}/bin/${source_fn}" "${BIN_LOCATION}/${target_fn}" unset source_fn target_fn done # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf "runghc${target_suffix}" "${BIN_LOCATION}/runhaskell${target_suffix}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf "hsc2hs-${myghcver}" "${BIN_LOCATION}/hsc2hs${target_suffix}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf "hp2ps-${myghcver}" "${BIN_LOCATION}/hp2ps${target_suffix}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf "hpc-${myghcver}" "${BIN_LOCATION}/hpc${target_suffix}" # not all bindists install haddock... if [ -e "${inst_location}/bin/haddock" ] ; then # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf "haddock-ghc${target_suffix}" "${BIN_LOCATION}/haddock${target_suffix}" fi status_message "Done" unset myghcver inst_location f target_suffix } ############################ #--[ Subcommand upgrade ]--# ############################ # @FUNCTION: upgrade # @USAGE: # @DESCRIPTION: # Downloads the latest version of this script and places it into # the given directory. upgrade() { target_location=$1 [ -e "${target_location}" ] || die "Destination \"${target_location}\" does not exist, cannot update script" status_message "Updating ${SCRIPT}" ( edo cd "$(mktemp -d -t ghcup.XXXXXXXX)" download "${SCRIPT_UPDATE_URL}" edo chmod +x ghcup edo mv -f ghcup "${target_location}"/ghcup ) || die "failed to install" status_message "Done" unset target_location } ####################### #--[ Subcommand rm ]--# ####################### # @FUNCTION: rm_ghc # @USAGE: # @DESCRIPTION: # Removes the given GHC version installed by ghcup. rm_ghc() { [ -z "$1" ] && die "Internal error: no argument given to rm_ghc" myghcver=$1 inst_location=$(get_ghc_location "${myghcver}") [ -z "${myghcver}" ] && die "We are paranoid, ghcver not set" if ghc_already_installed "${myghcver}" ; then [ -z "${inst_location}" ] && die "internal error: inst_location empty!" if ! ${FORCE} ; then if ! ask_for_confirmation "Really removing ${myghcver}? This will also recursively remove the following directory (please double-check): \"${inst_location}\"" ; then warning_message "Not removing GHC..." return 0 fi fi for f in "${BIN_LOCATION}"/*-"${myghcver}" ; do # https://tanguy.ortolo.eu/blog/article113/test-symlink [ ! -e "${f}" ] && [ ! -h "${f}" ] && { warning_message "No existing symlinks for ${myghcver} in ${BIN_LOCATION}, skipping" break } edo rm "${f}" done edo rm -r "${inst_location}" status_message "Successfully removed GHC ${myghcver}." # Only run set_ghc_major if there is at least one 8.6.x version left for 8.6. if [ -n "$(get_full_ghc_ver "${myghcver%.*}")" ] ; then set_ghc_major "${myghcver}" fi if [ -h "${BIN_LOCATION}/ghc-${myghcver%.*}" ] && [ ! -e "${BIN_LOCATION}/ghc-${myghcver%.*}" ] ; then # TODO: known_tools is not very robust, but we want to avoid accidentially deleting # unrelated things (even if those are dangling symlinks) known_tools="ghc ghci ghc-pkg haddock haddock-ghc runghc runhaskell hp2ps hpc hsc2hs" # remove dangling symlinks for ghc, ghci, ... for t in ${known_tools} ; do if [ -h "${BIN_LOCATION}/${t}-${myghcver%.*}" ] && [ ! -e "${BIN_LOCATION}/${t}-${myghcver%.*}" ] ; then edo rm "${BIN_LOCATION}/${t}-${myghcver%.*}" fi done unset t known_tools fi if [ -h "${BIN_LOCATION}/ghc" ] && [ ! -e "${BIN_LOCATION}/ghc" ] ; then warning_message "Currently active GHC is a dangling symlink, removing..." # TODO: known_tools is not very robust, but we want to avoid accidentially deleting # unrelated things (even if those are dangling symlinks) known_tools="ghc ghci ghc-pkg haddock haddock-ghc runghc runhaskell hp2ps hpc hsc2hs" # remove dangling symlinks for ghc, ghci, ... for t in ${known_tools} ; do if [ -h "${BIN_LOCATION}/${t}" ] && [ ! -e "${BIN_LOCATION}/${t}" ] ; then edo rm "${BIN_LOCATION}/${t}" fi done unset t known_tools warning_message "Done." warning_message "You may now want to set currently active GHC to a different version via:" warning_message " ghcup set " fi else warning_message "${myghcver} doesn't appear to be installed, skipping" fi unset myghcver inst_location f } ############################ #--[ Subcommand install ]--# ############################ # @FUNCTION: install_cabal # @USAGE: # @DESCRIPTION: # Installs the given cabal version. install_cabal() { [ -z "$1" ] && die "Internal error: no argument given to install_cabal" mycabalver=$1 myarch=$(get_arch) [ -z "${myarch}" ] && die "failed to get architecture" inst_location=$BIN_LOCATION download_url=$(get_download_url "cabal-install" "${mycabalver}") download_tarball_name=$(basename "${download_url}") if [ -z "${download_url}" ] ; then die "Could not find an appropriate download for the requested cabal-install-${mycabalver} on your system! Please report a bug at ${BUG_URL}" fi status_message "Installing cabal-install-${mycabalver} into \"${inst_location}\"" edo mkdir -p "${inst_location}" tmp_dir=$(mktemp -d -t ghcup.XXXXXXXX) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( if ${CACHING} ; then if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then download_to_cache "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi edo mv -f cabal "${inst_location}"/cabal if [ -e "${tmp_dir}/${download_tarball_name}" ] ; then rm "${tmp_dir}/${download_tarball_name}" fi ) || die "Failed to install cabal-install" status_message "Successfully installed cabal-install into" status_message " ${BIN_LOCATION}" status_message "" unset mycabalver myarch inst_location download_url download_tarball_name tmp_dir } # @FUNCTION: compile_ghc # @USAGE: [build.mk] # @DESCRIPTION: # Compile and installs the given GHC version with the # specified GHC bootstrap version. # Can additionally take a custom file that will be used # as build configuration. compile_ghc() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to compile_ghc" myghcver=$1 bootstrap_ghc=$2 inst_location=$(get_ghc_location "$1") [ -z "${inst_location}" ] && die "failed to get install location" download_url="https://downloads.haskell.org/~ghc/${myghcver}/ghc-${myghcver}-src.tar.xz" download_tarball_name=$(basename "${download_url}") if [ -n "$3" ] ; then case "$3" in /*) build_config=$3 ;; *) build_config="$(pwd)/$3" ;; esac [ -e "${build_config}" ] || die "specified build config \"${build_config}\" file does not exist!" fi if ghc_already_installed "${myghcver}" ; then if ${FORCE} ; then echo "GHC already installed in ${inst_location}, overwriting!" else die "GHC already installed in ${inst_location}, use --force to overwrite" fi fi status_message "Compiling GHC for version ${myghcver} from source" tmp_dir=$(mktemp -d -t ghcup.XXXXXXXX) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( if ${CACHING} ; then if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then download_to_cache "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi edo cd "./ghc-${myghcver}" if [ -n "${build_config}" ] ; then edo cat "${build_config}" > mk/build.mk else cat <<-EOF > mk/build.mk || die V=0 BUILD_MAN = NO BUILD_SPHINX_HTML = NO BUILD_SPHINX_PDF = NO HADDOCK_DOCS = YES GhcWithLlvmCodeGen = YES EOF fi edo ./boot edo ./configure --prefix="${inst_location}" --with-ghc="${bootstrap_ghc}" emake -j${JOBS} emake install # clean up edo cd .. [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" ) || { [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" die "Failed to install, consider updating this script via: ${SCRIPT} upgrade Also check https://ghc.haskell.org/trac/ghc/wiki/Building/Preparation/Linux for build requirements and follow the instructions." } for f in "${inst_location}"/bin/*-"${myghcver}" ; do [ -e "${f}" ] || die "Something went wrong, ${f} does not exist!" fn=$(basename "${f}") # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}/bin/${fn}" "${BIN_LOCATION}/${fn}" unset fn done # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/runhaskell "${BIN_LOCATION}/runhaskell-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hsc2hs "${BIN_LOCATION}/hsc2hs-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hp2ps "${BIN_LOCATION}/hp2ps-${myghcver}" # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/hpc "${BIN_LOCATION}/hpc-${myghcver}" if [ -e "${inst_location}/bin/haddock" ] ; then # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/haddock "${BIN_LOCATION}/haddock-${myghcver}" fi status_message "Done installing, run \"ghci-${myghcver}\" or set up your current GHC via: ${SCRIPT} set ${myghcver}" unset bootstrap_ghc inst_location f download_url download_tarball_name tmp_dir set_ghc_major "${myghcver}" unset myghcver } ############################### #--[ Subcommand debug-info ]--# ############################### # @FUNCTION: print_debug_info # @DESCRIPTION: # Print debug info (e.g. detected system/distro). print_debug_info() { echo "Script version: ${VERSION}" echo echo "Script variables:" echo " GHC install location: ${GHC_LOCATION}" echo " Binary install location: ${BIN_LOCATION}" echo " Tarball cache location: ${CACHE_LOCATION}" echo " Downloader: ${DOWNLOADER} ${DOWNLOADER_OPTS} " echo " Script update url: ${SCRIPT_UPDATE_URL}" echo " GHC download baseurl: ${GHC_DOWNLOAD_BASEURL}" echo " Meta download url ${META_DOWNLOAD_URL}" echo " Meta download format ${META_DOWNLOAD_FORMAT}" echo " Meta version url ${META_VERSION_URL}" echo " Meta version format ${META_VERSION_FORMAT}" echo echo "Detected system information:" echo " Architecture: $(get_arch)" echo " Distribution: $(get_distro_name)" echo " Distro alias: $(get_distro_alias "$(get_distro_name)")" echo " Distro version: $(get_distro_ver)" } ######################### #--[ Subcommand list ]--# ######################### # @FUNCTION: list # @USAGE: # @DESCRIPTION: # List available tools and their versions from upstream. list() { mytool=$1 raw_format=$2 criteria=$3 meta_file="$(get_meta_version_file)" [ -z "${meta_file}" ] && die "failed to get meta file" if ! ${raw_format} ; then printf "\\033[1;32m%s\\033[0m\\n" "Available versions:" fi lines=$( if [ "${mytool}" = "all" ] ; then awk " NF { if (\$1 != \"#\") { print \$1 \"\\t\" \$2 \"\\t\" \$3 } }" "${meta_file}" || die "awk failed!" else awk " NF { if (\$1 == \"${mytool}\") { print \$1 \"\\t\" \$2 \"\\t\" \$3 } }" "${meta_file}" || die "awk failed!" fi ) _print_me() { if ${raw_format} ; then printf "%s\\n" "$1" else if [ "$2" = "available" ] ; then printf "\\033[0;32m\342\234\224\\033[0m %s\\n" "$1" elif [ "$2" = "set" ] ; then printf "\\033[0;32m\342\234\224 \\033[0;34m%s\\033[0m\\n" "$1" elif [ "$2" = "unavailable" ] ; then printf "\\033[0;31m\342\234\227\\033[0m %s\\n" "$1" fi fi } if [ -z "${lines}" ] ; then (>&2 echo "Nothing found for tool ${mytool}") return fi echo "$lines" | while read -r l; do tool=$(echo "${l}" | cut -f1) version=$(echo "${l}" | cut -f2) if [ "${criteria}" = "set" ] ; then if [ "${tool}" = "ghc" ] && [ "${version}" = "$(show_ghc_installed)" ] ; then _print_me "${l}" "set" fi if [ "${tool}" = "cabal-install" ] && [ "${version}" = "$(show_cabal_installed)" ] ; then _print_me "${l}" "set" fi else if tool_already_installed "${tool}" "${version}" ; then if [ "${tool}" = "ghc" ] && [ "${version}" = "$(show_ghc_installed)" ] ; then _print_me "${l}" "set" elif [ "${tool}" = "cabal-install" ] && [ "${version}" = "$(show_cabal_installed)" ] ; then _print_me "${l}" "set" else _print_me "${l}" "available" fi else if [ "${criteria}" != "installed" ] ; then _print_me "${l}" "unavailable" fi fi fi done unset mytool meta_file l lines tool version raw_format installed_only criteria } ############################## #--[ Subcommand changelog ]--# ############################## # @FUNCTION: changelog_url # @USAGE: # @DESCRIPTION: # Print the changelog url for the given GHC version to stdout. # @STDOUT: the changelog url changelog_url() { [ -z "$1" ] && die "Internal error: no argument given to changelog" printf "https://downloads.haskell.org/~ghc/%s/docs/html/users_guide/%s-notes.html" "$1" "$1" } # @FUNCTION: changelog # @USAGE: # @DESCRIPTION: # Opens the changelog for the given ghc version via xdg-open. changelog() { [ -z "$1" ] && die "Internal error: no argument given to changelog" url=$(changelog_url "$1") xdg-open "${url}" || die "failed to xdg-open the following url: ${url}" unset url } ###################################### #--[ Subcommand print-system-reqs ]--# ###################################### # @FUNCTION: system_reqs_url # @USAGE: # @DESCRIPTION: # Mapping of distro-alias to system requirements URL. system_reqs_url() { [ -z "$1" ] && die "Internal error: no argument given to system_reqs_url" case "$1" in "alpine") printf "%s/.requirements/ghc/alpine" "${BASE_DOWNLOAD_URL}" ;; "debian"|"ubuntu") printf "%s/.requirements/ghc/ubuntu" "${BASE_DOWNLOAD_URL}" ;; "darwin") printf "%s/.requirements/ghc/darwin" "${BASE_DOWNLOAD_URL}" ;; *) printf "%s/.requirements/ghc/default" "${BASE_DOWNLOAD_URL}" ;; esac } # @FUNCTION: print_system_reqs # @DESCRIPTION: # Print the system requirements (approximation). print_system_reqs() { mydistro=$(get_distro_alias "$(get_distro_name)") reqs_url=$(system_reqs_url "${mydistro}") download_to_stdout "${reqs_url}" unset mydistro reqs_url } ####################### #--[ Sanity checks ]--# ####################### if [ -z "${GHCUP_INSTALL_BASE_PREFIX}" ] ; then die "GHCUP_INSTALL_BASE_PREFIX empty, cannot operate" fi if [ ! -d "${GHCUP_INSTALL_BASE_PREFIX}" ] ; then die "${GHCUP_INSTALL_BASE_PREFIX} does not exist" fi ############################################## #--[ Command line parsing and entry point ]--# ############################################## [ $# -lt 1 ] && usage while [ $# -gt 0 ] ; do case $1 in -v|--verbose) VERBOSE=true shift 1 if [ $# -lt 1 ] ; then usage fi ;; -V|--version) printf "%s\\n" "${VERSION}" exit 0;; --list-commands) echo "changelog compile debug-info install install-cabal list print-system-reqs rm set upgrade" exit 0;; -h|--help) usage;; -w|--wget) DOWNLOADER="wget" DOWNLOADER_OPTS="" DOWNLOADER_STDOUT_OPTS="-qO-" shift 1 if [ $# -lt 1 ] ; then usage fi ;; -c|--cache) CACHING=true shift 1 if [ $# -lt 1 ] ; then usage fi ;; *) ## startup tasks ## edo mkdir -p "${INSTALL_BASE}" edo mkdir -p "${BIN_LOCATION}" edo mkdir -p "${CACHE_LOCATION}" # clean up old meta files if [ -e "${CACHE_LOCATION}/$(basename "${META_VERSION_URL}")" ] ; then edo rm "${CACHE_LOCATION}/$(basename "${META_VERSION_URL}")" fi if [ -e "${CACHE_LOCATION}/$(basename "${META_DOWNLOAD_URL}")" ] ; then edo rm "${CACHE_LOCATION}/$(basename "${META_DOWNLOAD_URL}")" fi # check for available commands missing_commands="$(check_required_commands ${DOWNLOADER})" if [ -n "${missing_commands}" ] ; then die "Following commands are required, but missing, please install: ${missing_commands}" fi unset missing_commands case $1 in install) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) install_usage;; -f|--force) FORCE=true shift 1;; *) GHC_VER=$1 break;; esac done if [ -z "${GHC_VER}" ] ; then _tool_ver="$(get_tool_ver_from_tag "ghc" "recommended")" if [ -z "${_tool_ver}" ] ; then die "Could not find a recommended GHC version, please report a bug at ${BUG_URL}!" fi install_ghc "${_tool_ver}" else # could be a version or a tag, let's check if array_contains "${GHC_VER}" "$(known_tool_versions "ghc")" ; then install_ghc "${GHC_VER}" elif array_contains "${GHC_VER}" "$(known_tool_tags "ghc")" ; then install_ghc "$(get_tool_ver_from_tag "ghc" "${GHC_VER}")" else die "\"${GHC_VER}\" is not a known version or tag!" fi fi break;; set) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) set_usage;; *) GHC_VER=$1 break;; esac done if [ -z "${GHC_VER}" ] ; then _tool_ver="$(get_tool_ver_from_tag "ghc" "recommended")" if [ -z "${_tool_ver}" ] ; then die "Could not find a recommended GHC version, please report a bug at ${BUG_URL}!" fi set_ghc "${_tool_ver}" else # could be a version or a tag, let's check if array_contains "${GHC_VER}" "$(known_tool_versions "ghc")" ; then set_ghc "${GHC_VER}" elif array_contains "${GHC_VER}" "$(known_tool_tags "ghc")" ; then set_ghc "$(get_tool_ver_from_tag "ghc" "${GHC_VER}")" else die "\"${GHC_VER}\" is not a known version or tag!" fi fi break;; upgrade) IN_PLACE=false shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) upgrade_usage;; -i|--inplace) IN_PLACE=true shift 1 ;; *) TARGET_LOCATION=$1 break;; esac done if ${IN_PLACE} ; then upgrade "$(dirname "$(posix_realpath "${SOURCE}")")" elif [ -n "${TARGET_LOCATION}" ] ; then upgrade "${TARGET_LOCATION}" else upgrade "${BIN_LOCATION}" fi break;; rm) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) rm_usage;; -f|--force) FORCE=true shift 1;; *) GHC_VER=$1 break;; esac done [ -n "${GHC_VER}" ] || rm_usage rm_ghc "${GHC_VER}" break;; install-cabal) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) install_cabal_usage;; -f|--force) FORCE=true shift 1;; *) CABAL_VER=$1 break;; esac done if [ -n "${CABAL_VER}" ] ; then # could be a version or a tag, let's check if array_contains "${CABAL_VER}" "$(known_tool_versions "cabal-install")" ; then install_cabal "${CABAL_VER}" elif array_contains "${CABAL_VER}" "$(known_tool_tags "cabal-install")" ; then install_cabal "$(get_tool_ver_from_tag "cabal-install" "${CABAL_VER}")" else die "\"${CABAL_VER}\" is not a known version or tag!" fi else _cabal_ver="$(get_tool_ver_from_tag "cabal-install" "recommended")" if [ -z "${_cabal_ver}" ] ; then die "Could not find a recommended cabal-install version, please report a bug at ${BUG_URL}!" fi install_cabal "${_cabal_ver}" fi break;; compile) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) compile_usage;; -f|--force) FORCE=true shift 1;; -j|--jobs) JOBS=$2 shift 2;; -c|--build-config) BUILD_CONFIG=$2 shift 2;; *) GHC_VER=$1 BOOTSTRAP_GHC=$2 break;; esac done [ -n "${GHC_VER}" ] || compile_usage [ -n "${BOOTSTRAP_GHC}" ] || compile_usage compile_ghc "${GHC_VER}" "${BOOTSTRAP_GHC}" "${BUILD_CONFIG}" break;; debug-info) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) debug_info_usage;; *) debug_info_usage;; esac done print_debug_info break;; list) RAW_FORMAT=false TOOL="ghc" shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) list_usage;; -t|--tool) TOOL=$2 shift 2;; -r|--raw-format) RAW_FORMAT=true shift 1;; -c|--show-criteria) SHOW_CRITERIA=$2 shift 2;; *) list_usage;; esac done list "${TOOL}" ${RAW_FORMAT} "${SHOW_CRITERIA}" break;; changelog) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) changelog_usage;; -f|--force) FORCE=true shift 1;; *) GHC_VER=$1 break;; esac done if [ -z "${GHC_VER}" ] ; then _tool_ver="$(get_tool_ver_from_tag "ghc" "latest")" if [ -z "${_tool_ver}" ] ; then die "Could not find a latest GHC version, please report a bug at ${BUG_URL}!" fi changelog "${_tool_ver}" else # could be a version or a tag, let's check if array_contains "${GHC_VER}" "$(known_tool_versions "ghc")" ; then changelog "${GHC_VER}" elif array_contains "${GHC_VER}" "$(known_tool_tags "ghc")" ; then changelog "$(get_tool_ver_from_tag "ghc" "${GHC_VER}")" else die "\"${GHC_VER}\" is not a known version or tag!" fi fi break;; print-system-reqs) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) print_system_reqs_usage;; *) print_system_reqs_usage;; esac done print_system_reqs break;; *) usage;; esac break;; esac done # vim: tabstop=4 shiftwidth=4 expandtab